/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.v4.widget;

import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * Static library support version of the framework's
 * {@link android.widget.SimpleCursorAdapter}. Used to write apps that run on
 * platforms prior to Android 3.0. When running on Android 3.0 or above, this
 * implementation is still used; it does not try to switch to the framework's
 * implementation. See the framework SDK documentation for a class overview.
 */
public class SimpleCursorAdapter extends ResourceCursorAdapter {
	/**
	 * A list of columns containing the data to bind to the UI. This field
	 * should be made private, so it is hidden from the SDK. {@hide}
	 */
	protected int[] mFrom;
	/**
	 * A list of View ids representing the views to which the data must be
	 * bound. This field should be made private, so it is hidden from the SDK.
	 * {@hide}
	 */
	protected int[] mTo;

	private int mStringConversionColumn = -1;
	private CursorToStringConverter mCursorToStringConverter;
	private ViewBinder mViewBinder;

	String[] mOriginalFrom;

	/**
	 * Constructor the enables auto-requery.
	 * 
	 * @deprecated This option is discouraged, as it results in Cursor queries
	 *             being performed on the application's UI thread and thus can
	 *             cause poor responsiveness or even Application Not Responding
	 *             errors. As an alternative, use
	 *             {@link android.app.LoaderManager} with a
	 *             {@link android.content.CursorLoader}.
	 */
	@Deprecated
	public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
		super(context, layout, c);
		mTo = to;
		mOriginalFrom = from;
		findColumns(from);
	}

	/**
	 * Standard constructor.
	 * 
	 * @param context
	 *            The context where the ListView associated with this
	 *            SimpleListItemFactory is running
	 * @param layout
	 *            resource identifier of a layout file that defines the views
	 *            for this list item. The layout file should include at least
	 *            those named views defined in "to"
	 * @param c
	 *            The database cursor. Can be null if the cursor is not
	 *            available yet.
	 * @param from
	 *            A list of column names representing the data to bind to the
	 *            UI. Can be null if the cursor is not available yet.
	 * @param to
	 *            The views that should display column in the "from" parameter.
	 *            These should all be TextViews. The first N views in this list
	 *            are given the values of the first N columns in the from
	 *            parameter. Can be null if the cursor is not available yet.
	 * @param flags
	 *            Flags used to determine the behavior of the adapter, as per
	 *            {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
	 */
	public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
		super(context, layout, c, flags);
		mTo = to;
		mOriginalFrom = from;
		findColumns(from);
	}

	/**
	 * Binds all of the field names passed into the "to" parameter of the
	 * constructor with their corresponding cursor columns as specified in the
	 * "from" parameter.
	 * 
	 * Binding occurs in two phases. First, if a
	 * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
	 * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
	 * is invoked. If the returned value is true, binding has occured. If the
	 * returned value is false and the view to bind is a TextView,
	 * {@link #setViewText(TextView, String)} is invoked. If the returned value
	 * is false and the view to bind is an ImageView,
	 * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
	 * binding can be found, an {@link IllegalStateException} is thrown.
	 * 
	 * @throws IllegalStateException
	 *             if binding cannot occur
	 * 
	 * @see android.widget.CursorAdapter#bindView(android.view.View,
	 *      android.content.Context, android.database.Cursor)
	 * @see #getViewBinder()
	 * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
	 * @see #setViewImage(ImageView, String)
	 * @see #setViewText(TextView, String)
	 */
	@Override
	public void bindView(View view, Context context, Cursor cursor) {
		final ViewBinder binder = mViewBinder;
		final int count = mTo.length;
		final int[] from = mFrom;
		final int[] to = mTo;

		for (int i = 0; i < count; i++) {
			final View v = view.findViewById(to[i]);
			if (v != null) {
				boolean bound = false;
				if (binder != null) {
					bound = binder.setViewValue(v, cursor, from[i]);
				}

				if (!bound) {
					String text = cursor.getString(from[i]);
					if (text == null) {
						text = "";
					}

					if (v instanceof TextView) {
						setViewText((TextView) v, text);
					} else if (v instanceof ImageView) {
						setViewImage((ImageView) v, text);
					} else {
						throw new IllegalStateException(v.getClass().getName() + " is not a " + " view that can be bounds by this SimpleCursorAdapter");
					}
				}
			}
		}
	}

	/**
	 * Returns the {@link ViewBinder} used to bind data to views.
	 * 
	 * @return a ViewBinder or null if the binder does not exist
	 * 
	 * @see #bindView(android.view.View, android.content.Context,
	 *      android.database.Cursor)
	 * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
	 */
	public ViewBinder getViewBinder() {
		return mViewBinder;
	}

	/**
	 * Sets the binder used to bind data to views.
	 * 
	 * @param viewBinder
	 *            the binder used to bind data to views, can be null to remove
	 *            the existing binder
	 * 
	 * @see #bindView(android.view.View, android.content.Context,
	 *      android.database.Cursor)
	 * @see #getViewBinder()
	 */
	public void setViewBinder(ViewBinder viewBinder) {
		mViewBinder = viewBinder;
	}

	/**
	 * Called by bindView() to set the image for an ImageView but only if there
	 * is no existing ViewBinder or if the existing ViewBinder cannot handle
	 * binding to an ImageView.
	 * 
	 * By default, the value will be treated as an image resource. If the value
	 * cannot be used as an image resource, the value is used as an image Uri.
	 * 
	 * Intended to be overridden by Adapters that need to filter strings
	 * retrieved from the database.
	 * 
	 * @param v
	 *            ImageView to receive an image
	 * @param value
	 *            the value retrieved from the cursor
	 */
	public void setViewImage(ImageView v, String value) {
		try {
			v.setImageResource(Integer.parseInt(value));
		} catch (NumberFormatException nfe) {
			v.setImageURI(Uri.parse(value));
		}
	}

	/**
	 * Called by bindView() to set the text for a TextView but only if there is
	 * no existing ViewBinder or if the existing ViewBinder cannot handle
	 * binding to an TextView.
	 * 
	 * Intended to be overridden by Adapters that need to filter strings
	 * retrieved from the database.
	 * 
	 * @param v
	 *            TextView to receive text
	 * @param text
	 *            the text to be set for the TextView
	 */
	public void setViewText(TextView v, String text) {
		v.setText(text);
	}

	/**
	 * Return the index of the column used to get a String representation of the
	 * Cursor.
	 * 
	 * @return a valid index in the current Cursor or -1
	 * 
	 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
	 * @see #setStringConversionColumn(int)
	 * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
	 * @see #getCursorToStringConverter()
	 */
	public int getStringConversionColumn() {
		return mStringConversionColumn;
	}

	/**
	 * Defines the index of the column in the Cursor used to get a String
	 * representation of that Cursor. The column is used to convert the Cursor
	 * to a String only when the current CursorToStringConverter is null.
	 * 
	 * @param stringConversionColumn
	 *            a valid index in the current Cursor or -1 to use the default
	 *            conversion mechanism
	 * 
	 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
	 * @see #getStringConversionColumn()
	 * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
	 * @see #getCursorToStringConverter()
	 */
	public void setStringConversionColumn(int stringConversionColumn) {
		mStringConversionColumn = stringConversionColumn;
	}

	/**
	 * Returns the converter used to convert the filtering Cursor into a String.
	 * 
	 * @return null if the converter does not exist or an instance of
	 *         {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
	 * 
	 * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
	 * @see #getStringConversionColumn()
	 * @see #setStringConversionColumn(int)
	 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
	 */
	public CursorToStringConverter getCursorToStringConverter() {
		return mCursorToStringConverter;
	}

	/**
	 * Sets the converter used to convert the filtering Cursor into a String.
	 * 
	 * @param cursorToStringConverter
	 *            the Cursor to String converter, or null to remove the
	 *            converter
	 * 
	 * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
	 * @see #getStringConversionColumn()
	 * @see #setStringConversionColumn(int)
	 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
	 */
	public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
		mCursorToStringConverter = cursorToStringConverter;
	}

	/**
	 * Returns a CharSequence representation of the specified Cursor as defined
	 * by the current CursorToStringConverter. If no CursorToStringConverter has
	 * been set, the String conversion column is used instead. If the conversion
	 * column is -1, the returned String is empty if the cursor is null or
	 * Cursor.toString().
	 * 
	 * @param cursor
	 *            the Cursor to convert to a CharSequence
	 * 
	 * @return a non-null CharSequence representing the cursor
	 */
	@Override
	public CharSequence convertToString(Cursor cursor) {
		if (mCursorToStringConverter != null) {
			return mCursorToStringConverter.convertToString(cursor);
		} else if (mStringConversionColumn > -1) {
			return cursor.getString(mStringConversionColumn);
		}

		return super.convertToString(cursor);
	}

	/**
	 * Create a map from an array of strings to an array of column-id integers
	 * in mCursor. If mCursor is null, the array will be discarded.
	 * 
	 * @param from
	 *            the Strings naming the columns of interest
	 */
	private void findColumns(String[] from) {
		if (mCursor != null) {
			int i;
			int count = from.length;
			if (mFrom == null || mFrom.length != count) {
				mFrom = new int[count];
			}
			for (i = 0; i < count; i++) {
				mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]);
			}
		} else {
			mFrom = null;
		}
	}

	@Override
	public Cursor swapCursor(Cursor c) {
		Cursor res = super.swapCursor(c);
		// rescan columns in case cursor layout is different
		findColumns(mOriginalFrom);
		return res;
	}

	/**
	 * Change the cursor and change the column-to-view mappings at the same
	 * time.
	 * 
	 * @param c
	 *            The database cursor. Can be null if the cursor is not
	 *            available yet.
	 * @param from
	 *            A list of column names representing the data to bind to the
	 *            UI. Can be null if the cursor is not available yet.
	 * @param to
	 *            The views that should display column in the "from" parameter.
	 *            These should all be TextViews. The first N views in this list
	 *            are given the values of the first N columns in the from
	 *            parameter. Can be null if the cursor is not available yet.
	 */
	public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
		mOriginalFrom = from;
		mTo = to;
		super.changeCursor(c);
		findColumns(mOriginalFrom);
	}

	/**
	 * This class can be used by external clients of SimpleCursorAdapter to bind
	 * values fom the Cursor to views.
	 * 
	 * You should use this class to bind values from the Cursor to views that
	 * are not directly supported by SimpleCursorAdapter or to change the way
	 * binding occurs for views supported by SimpleCursorAdapter.
	 * 
	 * @see SimpleCursorAdapter#bindView(android.view.View,
	 *      android.content.Context, android.database.Cursor)
	 * @see SimpleCursorAdapter#setViewImage(ImageView, String)
	 * @see SimpleCursorAdapter#setViewText(TextView, String)
	 */
	public static interface ViewBinder {
		/**
		 * Binds the Cursor column defined by the specified index to the
		 * specified view.
		 * 
		 * When binding is handled by this ViewBinder, this method must return
		 * true. If this method returns false, SimpleCursorAdapter will attempts
		 * to handle the binding on its own.
		 * 
		 * @param view
		 *            the view to bind the data to
		 * @param cursor
		 *            the cursor to get the data from
		 * @param columnIndex
		 *            the column at which the data can be found in the cursor
		 * 
		 * @return true if the data was bound to the view, false otherwise
		 */
		boolean setViewValue(View view, Cursor cursor, int columnIndex);
	}

	/**
	 * This class can be used by external clients of SimpleCursorAdapter to
	 * define how the Cursor should be converted to a String.
	 * 
	 * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
	 */
	public static interface CursorToStringConverter {
		/**
		 * Returns a CharSequence representing the specified Cursor.
		 * 
		 * @param cursor
		 *            the cursor for which a CharSequence representation is
		 *            requested
		 * 
		 * @return a non-null CharSequence representing the cursor
		 */
		CharSequence convertToString(Cursor cursor);
	}

}
